package se.webbzon.boltzmann.input;

import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;

import net.java.games.input.Component;
import net.java.games.input.Controller;
import net.java.games.input.Event;
import se.webbzon.oschi01.jinput.ControllerEventListener;
import static se.webbzon.boltzmann.input.GamepadButtonInfo.Direction.*;

public class GamepadInput extends Input implements ControllerEventListener {
	
	// The required value of the stick before it triggers
	private static final double motionLimit = 0.3;
	private static final double pressLimit = 0.6;
	
	// A map containing all gamepad/stick inputs and button enumerations
	private final Map<Component.Identifier.Button,Button> buttonMap;
	
	// A map containing information about the buttons
	private final Map<Button,ButtonInfo> infoMap;
	
	// Whatever this gamepad is in fact a stick
	private final boolean stick;
	
	// The name of the gamepad input
	private final String name;
	
	// The primary x and y axis
	private final Component.Identifier primaryX, primaryY;
	
	// The last position of the primary axis
	private volatile double px, py;
	
	// The secondary x and y axis
	private final Component.Identifier secondaryX, secondaryY;
	
	// The last position of the secondary axis
	private volatile double sx, sy;
	
	// The button to bind via the next key event
	private Button binding;
	
	/*============================================================
	Constructors
	============================================================*/
	
	public GamepadInput(Controller controller) {
		name = controller.getName();
		stick = (controller.getType() == Controller.Type.STICK);
		buttonMap = new HashMap<Component.Identifier.Button,Button>(16); 
		infoMap = new HashMap<Button,ButtonInfo>(16);
		binding = null;
		px = 0;
		py = 0;
		sx = 0;
		sy = 0;
		
		Component.Identifier pX = null, pY = null, sX = null, sY = null;
		Component[] components = controller.getComponents();
		for (Component component : components) {
			Component.Identifier id = component.getIdentifier();
			if (id == Component.Identifier.Axis.X)
				pX = id;
			else if (id == Component.Identifier.Axis.Y)
				pY = id;
			else if (id == Component.Identifier.Axis.RZ)
				sX = id;
			else if (id == Component.Identifier.Axis.Z)
				sY = id;
		}
		primaryX = pX;
		primaryY = pY;
		secondaryX = sX;
		secondaryY = sY;
		
		infoMap.put(Button.LEFT, new GamepadButtonInfo(Left));
		infoMap.put(Button.RIGHT, new GamepadButtonInfo(Right));
		infoMap.put(Button.UP, new GamepadButtonInfo(Up));
		infoMap.put(Button.DOWN, new GamepadButtonInfo(Down));
		if (stick)
			initializeStickMap();
		else
			initializeGamepadMap();
	}
	
	/*============================================================
	Public Methods
	============================================================*/

	@Override public String getName() {
		if (stick)
			return name + " (Stick)";
		else
			return name;
	}
	
	@Override public boolean hasPrimaryAxis() {
		return (primaryX != null && primaryY != null);
	}

	@Override public AxisInfo getPrimaryAxisInfo() {
		return new GamepadAxisInfo(true);
	}

	@Override public boolean hasSecondaryAxis() {
		return (secondaryX != null && secondaryY != null);
	}

	@Override public AxisInfo getSecondaryAxisInfo() {
		return new GamepadAxisInfo(false);
	}

	@Override public synchronized ButtonInfo getButtonInfo(Button button) {
		return infoMap.get(button);
	}

	@Override public synchronized boolean supportsBinding(Button button) {
		if (button == Button.LEFT || button == Button.RIGHT ||
			button == Button.UP || button == Button.DOWN)
			return false;
		else
			return true;
	}

	@Override public synchronized boolean startBinding(Button button) {
		if (binding != null)
			return false;
		else {
			binding = button;
			return true;
		}
	}

	@Override public synchronized boolean stopBindings() {
		binding = null;
		return true;
	}
	
	@Override public void controllerChanged(Controller controller, Event event) {
		double oldPy = py, oldPx = px;
		double oldSy = sy, oldSx = sx;
		Component.Identifier id = event.getComponent().getIdentifier();
		if (id == primaryX) {
			px = event.getValue();
		} else if (id == primaryY) {
			py = -event.getValue();
		} else if (id == secondaryX) {
			sx = event.getValue();
		} else if (id == secondaryY) {
			sy = -event.getValue();
		} else if (id instanceof Component.Identifier.Button) {
			Component.Identifier.Button bid = (Component.Identifier.Button) id;
			Button binding = getBinding();
			if (binding != null) {
				bindButton(bid,binding);
				stopBindings();
				acceptBinding(binding);
			} else {
				Button b = buttonMap.get(bid);
				if (b != null) {
					if (event.getValue() == 0)
						releaseButton(b);
					else
						pressButton(b);
				}
			}
		}
		
		if (px != oldPx || py != oldPy) {
			double dir = Math.atan2(py, px);
			if (dir < 0) dir+=2*Math.PI;
			double value = Math.hypot(px, py);
			if (value < motionLimit) value = 0;
			else if (value > 1) value = 1;
			this.setPrimaryAxis(dir, value);
			updateDirection(oldPx,oldPy,px,py);
		} else if (sx != oldSx || sy != oldSy) {
			double dir = Math.atan2(sy, sx);
			if (dir < 0) dir+=2*Math.PI;
			double value = Math.hypot(sx, sy);
			if (value < motionLimit) value = 0;
			else if (value > 1) value = 1;
			this.setSecondaryAxis(dir, value);
		}
	}
	
	/*============================================================
	Private Methods
	============================================================*/
	
	/** Updates the direction by pressing or releasing the left, right, 
	 * up or down button accordingly to the direction of the primary
	 * axis. **/
	private void updateDirection(double oldPx, double oldPy, double px, double py) {
		if (oldPx > -pressLimit && px <= -pressLimit)
			pressButton(Button.LEFT);
		else if (oldPx <= -pressLimit && px > -pressLimit)
			releaseButton(Button.LEFT);
		
		if (oldPx < pressLimit && px >= pressLimit)
			pressButton(Button.RIGHT);
		else if (oldPx >= pressLimit && px < pressLimit)
			releaseButton(Button.RIGHT);
		
		if (oldPy > -pressLimit && py <= -pressLimit)
			pressButton(Button.DOWN);
		else if (oldPy <= -pressLimit && py > -pressLimit)
			releaseButton(Button.DOWN);
		
		if (oldPy < pressLimit && py >= pressLimit)
			pressButton(Button.UP);
		else if (oldPy >= pressLimit && py < pressLimit)
			releaseButton(Button.UP);
	}
	
	/** Returns the button associated with the last unhandled binding call.
	 * If there is no unhandled binding call then null is returned. **/
	private synchronized Button getBinding() {
		return binding;
	}
	
	/** Binds the given gamepad button identifier to the provided button. **/
	private synchronized void bindButton(Component.Identifier.Button b, Button button) {
		// Unbind old button
		Component.Identifier.Button oldButton = null;
		for (Entry<Component.Identifier.Button,Button> entry : buttonMap.entrySet()) {
			if (entry.getValue() == button) {
				oldButton = entry.getKey();
				break;
			}
		}
		if (oldButton != null)
			buttonMap.remove(oldButton);
		
		// Bind new button
		buttonMap.put(b, button);
		infoMap.put(button, new GamepadButtonInfo(b));
	}
	
	/** Initializes the button map for a gamepad. **/
	private void initializeGamepadMap() {
		bindButton(Component.Identifier.Button.A, Button.JUMP);
		bindButton(Component.Identifier.Button.B, Button.SWITCH);
		bindButton(Component.Identifier.Button.Y, Button.SPECIAL);
		bindButton(Component.Identifier.Button.X, Button.ACTION);
		
		bindButton(Component.Identifier.Button.START, Button.INVENTORY);
		bindButton(Component.Identifier.Button.LEFT_THUMB, Button.PREVIOUS_ITEM);
		bindButton(Component.Identifier.Button.RIGHT_THUMB, Button.NEXT_ITEM);
		bindButton(Component.Identifier.Button.LEFT_THUMB2, Button.PREVIOUS_CHARACTER);
		bindButton(Component.Identifier.Button.RIGHT_THUMB2, Button.NEXT_CHARACTER);
	}

	/** Initializes the button map for a stick. **/
	private void initializeStickMap() {
		bindButton(Component.Identifier.Button._1, Button.JUMP);
		bindButton(Component.Identifier.Button._3, Button.SWITCH);
		bindButton(Component.Identifier.Button._4, Button.SPECIAL);
		bindButton(Component.Identifier.Button._0, Button.ACTION);
		
		bindButton(Component.Identifier.Button._5, Button.INVENTORY);
		bindButton(Component.Identifier.Button._6, Button.PREVIOUS_ITEM);
		bindButton(Component.Identifier.Button._7, Button.NEXT_ITEM);
		bindButton(Component.Identifier.Button._8, Button.PREVIOUS_CHARACTER);
		bindButton(Component.Identifier.Button._9, Button.NEXT_CHARACTER);
	}

}
